iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 3
1

概念想法

如果程式剛初始化的時候發生crash,可能還沒有太大傷害,但若程式執行起來已經上線一陣子,正在處理到一半的資料突然中斷,麻煩可就大了。

多年來的開發經驗,心得就是:沒有什麼是保證絕對不會壞。

該壞的就壞,你可能無法做些什麼,但是可以的話,控制住損害範圍,保護無辜或者不相關的部分。

舉個例子,API server 服務進來的request,這些request彼此不相關,各自獨立也不互相影響。倘若其中一個request的程式處理會導致server crash掉,那麼將直接導致目前所有正在處理的request也跟著crash,後續的request也因為server死掉,完全無法處理,整體而言實在糟糕至極。

相反,若只有該request會發生問題,或者某個api因為某種情況而無法正常服務,卻不會影響其他功能,體驗上就會好很多。

為什麼說runtime(執行期間)才知道錯誤

因為有些結構或者實體在執行期間才會建立,或者進行了某些處理,結構裡面才有資料。

於是在發生了某了錯誤,可能導致結構無法如預期實體化,你卻使用這個struct的某個method,或者資料處理沒有按預想的發展,而強硬地用index取撈取等等,可能都會發生讓程式視為嚴重的錯誤,往外拋出panic。

這些錯誤除了race condition可以在build前下指令偵測以外,儘量以防衛性的方式撰寫程式碼,剩下的還是加上recover比較保險。

recover要怎麼加

recover 一定要搭配defer才能發揮作用,不可單獨只使用recover,然後recover的程式碼有寫但還沒執行到的話,就遇到崩潰也沒路用(有時候運氣就是這麼不好,諸君千萬別心存僥倖)。

策略上通常會想要在最不可能崩潰或出問題的地方,先執行過這段recover程式碼確保安全,所以做recover處理的程式區塊,往往擺在function進入的一開始。

palyground

寫法如下

	defer func() {
		if err := recover(); err != nil {
			fmt.Println("Error:", err)
			// 或者自定義的處理
		}
	}()

但由於panic已經被你寫的recover接到,所以預設就不會打印有發生問題的trace,自己加上問題點才好找。

    	defer func() {
		if err := recover(); err != nil {
			fmt.Println("Error:", err)
			log.Println("stacktrace from panic: \n" + string(debug.Stack()))

		}
	}()

那麼關於panic,那我只要在main function 加上recover就萬無一失了嗎

這分為兩個面向討論

1. 業務面向:

panic 並非萬惡之物,有recover並不代表程式沒有問題,或者就解決問題,若在深思熟慮的規劃之下,竟然還有未知的原因,造成panic跳出的時候,要不要用recover接起來,其實看各位專案的需求。

我們的專案開發,傾向在初始化時,譬如說DB連線建立,發生重大的錯誤直接讓panic丟出來,造成程式crash,好處是發佈後的第一時間,我們就知道程式的狀況,連基本的連線都有問題,後續做什麼都是錯的。

而程式順利啟動後,會傾向準備好recover,避免一個部分造成的錯誤,影響到其他正常的環節,執行到一半的資料錯誤事後修補往往非常困難。

2. 程式面向:

若程式有開goroutine處理,要注意recover不寫在goroutine裡面是無效的,是goroutine裡面發生panic的話,即使main function有寫recover,也不會有作用。

如下面小小的範例,recover B 沒把註解打開,發生了panic還是依舊爆炸給你看。

package main

import (
	"fmt"
	"sync"
)

func main() {

	// recover main
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("Error:", err)
		}
	}()

	var wg sync.WaitGroup
	testA(&wg)
	wg.Wait()
}

func testA(wg *sync.WaitGroup) {

	// recover  A-1
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("Error:", err)
			wg.Done()
		}
	}()

	for i := 0; i < 10; i++ {
		// recover  A-2
		defer func() {
			if err := recover(); err != nil {
				fmt.Println("Error:", err)
				wg.Done()
			}
		}()

		count := i
		wg.Add(1)
		go testB(wg, count)
	}
}

func testB(wg *sync.WaitGroup, count int) {

	// recover B
	// defer func() {
	// 	if err := recover(); err != nil {
	// 		fmt.Println("Error:", err)
	// 		wg.Done()
	// 	}
	// }()

	fmt.Println(count)
	if count == 5 {
		panic("occur panic!")
	}

	fmt.Println("doing something")
	wg.Done()
}

panic 也可以自己產生

panic("occur panic!")

簡簡單單,使用panic函式就可以自己製造panic,panic一旦產生,接下來的程式就不會繼續執行,而且會持續往外拋,直到有recover接到或者程式crash。

這有點像php的throw,以及搭配try catch,接到exception的感覺。

在很巢狀或深層的函式,如果發生了『error』,又想要將這個錯誤訊息往上傳遞到很外面的時候,在Golang只能一層一層慢慢傳,對於程式撰寫、追蹤、維護上可能會非常非常不方便,所以有人也會採用主動製造panic的方式,直接打到最外面讓安排好的recover接受後,再另做處理。

但是這樣的方式,還是看各自的需求而制定,見仁見智,不能絕對說好或壞。

下篇,筆者跟大家介紹幾個會造成panic的常見情況


上一篇
Day2 .[重災經驗篇] 談談Golang的程式crash
下一篇
Day4 .[重災經驗篇] 常見的panic造成原因
系列文
Let's Eat GO ! 實務開發雜談by Golang30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言